package com.lost.sos.cfg;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

public class ConfigsProvider extends ContentProvider {
    private static final boolean DEBUG = false;
    private static final String LOG_TAG = "ConfigsProvider";
    private static final int MUTATION_OPERATION_INSERT = 1;
    private static final int MUTATION_OPERATION_DELETE = 2;
    private static final int MUTATION_OPERATION_UPDATE = 3;

    private static final String[] ALL_COLUMNS = new String[] {
            Configs.NameValueTable._ID,
            Configs.NameValueTable.NAME,
            Configs.NameValueTable.VALUE
    };

    private static final Bundle NULL_CONFIG = ForPair(Configs.NameValueTable.VALUE, null);

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private ConfigsRegistry mConfigsRegistry;

    @Override
    public boolean onCreate() {
        synchronized (mLock) {
            mConfigsRegistry = new ConfigsRegistry();
        }
        return true;
    }

    @Override
    public Bundle call(String method, String name, Bundle args) {
        switch (method) {
            case Configs.CALL_METHOD_GET: {
                ConfigsState.Config config = getConfig(name);
                return packageValueForCallResult(config);
            }
            case Configs.CALL_METHOD_PUT: {
                String value = getConfigValue(args);
                insertConfig(name, value);
                break;
            }
            default: {
                Log.w(LOG_TAG, "call() with invalid method: " + method);
            } break;
        }

        return null;
    }

    @Override
    public String getType(Uri uri) {
        Arguments args = new Arguments(uri, null, null, true);
        if (TextUtils.isEmpty(args.name)) {
            return "vnd.lost.cursor.dir/config";
        } else {
            return "vnd.lost.cursor.item/config";
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String where, String[] whereArgs,
            String order) {
        Arguments args = new Arguments(uri, where, whereArgs, true);
        String[] normalizedProjection = normalizeProjection(projection);
        if (args.name != null) {
            ConfigsState.Config config = getConfig(args.name);
            return packageConfigForQuery(config, normalizedProjection);
        } else {
            return getAllConfigs(projection);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        String name = values.getAsString(Configs.Config.NAME);
        if (!isKeyValid(name)) {
            return null;
        }
        String value = values.getAsString(Configs.Config.VALUE);
        if (insertConfig(name, value)) {
            return Uri.withAppendedPath(Configs.Config.CONTENT_URI, name);
        }
        return null;
    }

    @Override
    public int bulkInsert(Uri uri, ContentValues[] allValues) {
        int insertionCount = 0;
        final int valuesCount = allValues.length;
        for (int i = 0; i < valuesCount; i++) {
            ContentValues values = allValues[i];
            if (insert(uri, values) != null) {
                insertionCount++;
            }
        }
        return insertionCount;
    }

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        Arguments args = new Arguments(uri, where, whereArgs, false);
        if (!isKeyValid(args.name)) {
            return 0;
        }
        return deleteConfig(args.name) ? 1 : 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        Arguments args = new Arguments(uri, where, whereArgs, false);
        String name = values.getAsString(Configs.Config.NAME);
        if (!isKeyValid(name)) {
            return 0;
        }
        String value = values.getAsString(Configs.Config.VALUE);
        return updateConfig(args.name, value) ? 1 : 0;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        throw new FileNotFoundException("Direct file access not supported");
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        synchronized (mLock) {
            final long identity = Binder.clearCallingIdentity();
            try {
                dump(pw);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
    }

    private void dump(PrintWriter pw) {
        pw.println("CONFIGS");
        Cursor globalCursor = getAllConfigs(ALL_COLUMNS);
        dumpConfigs(globalCursor, pw);
        pw.println();
    }

    private void dumpConfigs(Cursor cursor, PrintWriter pw) {
        if (cursor == null || !cursor.moveToFirst()) {
            return;
        }

        final int idColumnIdx = cursor.getColumnIndex(Configs.NameValueTable._ID);
        final int nameColumnIdx = cursor.getColumnIndex(Configs.NameValueTable.NAME);
        final int valueColumnIdx = cursor.getColumnIndex(Configs.NameValueTable.VALUE);

        do {
            pw.append("_id:").append(toDumpString(cursor.getString(idColumnIdx)));
            pw.append(" name:").append(toDumpString(cursor.getString(nameColumnIdx)));
            pw.append(" value:").append(toDumpString(cursor.getString(valueColumnIdx)));
            pw.println();
        } while (cursor.moveToNext());
    }

    private static String toDumpString(String s) {
        if (s != null) {
            return s;
        }
        return "{null}";
    }

    private Cursor getAllConfigs(String[] projection) {
        if (DEBUG) {
            Log.v(LOG_TAG, "getAllConfigs()");
        }

        synchronized (mLock) {
            // Get the configs.
            ConfigsState ConfigsState = mConfigsRegistry.getConfigsStateLocked();

            List<String> names = ConfigsState.getConfigNamesLocked();

            final int nameCount = names.size();

            String[] normalizedProjection = normalizeProjection(projection);
            MatrixCursor result = new MatrixCursor(normalizedProjection, nameCount);

            // Anyone can get the global configs, so no security checks.
            for (int i = 0; i < nameCount; i++) {
                String name = names.get(i);
                com.lost.sos.cfg.ConfigsState.Config config = ConfigsState.getConfigLocked(name);
                appendConfigToCursor(result, config);
            }

            return result;
        }
    }

    private ConfigsState.Config getConfig(String name) {
        if (DEBUG) {
            Log.v(LOG_TAG, "getConfig(" + name + ")");
        }

        // Get the value.
        synchronized (mLock) {
            return mConfigsRegistry.getConfigLocked(name);
        }
    }

    private boolean updateConfig(String name, String value) {
        if (DEBUG) {
            Log.v(LOG_TAG, "updateConfig(" + name + ", " + value + ")");
        }
        return mutateConfig(name, value, MUTATION_OPERATION_UPDATE);
    }

    private boolean insertConfig(String name, String value) {
        if (DEBUG) {
            Log.v(LOG_TAG, "insertConfig(" + name + ", " + value + ")");
        }
        return mutateConfig(name, value, MUTATION_OPERATION_INSERT);
    }

    private boolean deleteConfig(String name) {
        if (DEBUG) {
            Log.v(LOG_TAG, "deleteConfigLocked(" + name + ")");
        }
        return mutateConfig(name, null, MUTATION_OPERATION_DELETE);
    }

    private boolean mutateConfig(String name, String value, int operation) {
        // Make sure the caller can change the configs - treated as secure.
        enforceWritePermission(Configs.WRITE_CONFIGS);
        // Perform the mutation.
        synchronized (mLock) {
            switch (operation) {
                case MUTATION_OPERATION_INSERT: {
                    return mConfigsRegistry.insertConfigLocked(name, value);
                }

                case MUTATION_OPERATION_DELETE: {
                    return mConfigsRegistry.deleteConfigLocked(name);
                }

                case MUTATION_OPERATION_UPDATE: {
                    return mConfigsRegistry.updateConfigLocked(name, value);
                }
            }
        }

        return false;
    }

    private void enforceWritePermission(String permission) {
        if (getContext().checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Permission denial: writing to configs requires:"
                    + permission);
        }
    }

    public static Bundle ForPair(String key, String value) {
        Bundle b = new Bundle(1);
        b.putString(key, value);
        return b;
    }

    private static Bundle packageValueForCallResult(ConfigsState.Config config) {
        if (config == null) {
            return NULL_CONFIG;
        }
        return ForPair(Configs.NameValueTable.VALUE, config.getValue());
    }

    private static String getConfigValue(Bundle args) {
        return (args != null) ? args.getString(Configs.NameValueTable.VALUE) : null;
    }

    private static MatrixCursor packageConfigForQuery(ConfigsState.Config config, String[] projection) {
        if (config == null) {
            return new MatrixCursor(projection, 0);
        }
        MatrixCursor cursor = new MatrixCursor(projection, 1);
        appendConfigToCursor(cursor, config);
        return cursor;
    }

    private static String[] normalizeProjection(String[] projection) {
        if (projection == null) {
            return ALL_COLUMNS;
        }

        final int columnCount = projection.length;
        for (int i = 0; i < columnCount; i++) {
            String column = projection[i];
            if (!ArrayUtils.contains(ALL_COLUMNS, column)) {
                throw new IllegalArgumentException("Invalid column: " + column);
            }
        }

        return projection;
    }

    private static void appendConfigToCursor(MatrixCursor cursor, ConfigsState.Config config) {
        final int columnCount = cursor.getColumnCount();

        String[] values =  new String[columnCount];

        for (int i = 0; i < columnCount; i++) {
            String column = cursor.getColumnName(i);

            switch (column) {
                case Configs.NameValueTable._ID: {
                    values[i] = config.getId();
                } break;

                case Configs.NameValueTable.NAME: {
                    values[i] = config.getName();
                } break;

                case Configs.NameValueTable.VALUE: {
                    values[i] = config.getValue();
                } break;
            }
        }

        cursor.addRow(values);
    }

    private static boolean isKeyValid(String key) {
        return !(TextUtils.isEmpty(key) || ConfigsState.isBinary(key));
    }

    private static final class Arguments {
        private static final Pattern WHERE_PATTERN_WITH_PARAM_NO_BRACKETS =
                Pattern.compile("[\\s]*name[\\s]*=[\\s]*\\?[\\s]*");

        private static final Pattern WHERE_PATTERN_WITH_PARAM_IN_BRACKETS =
                Pattern.compile("[\\s]*\\([\\s]*name[\\s]*=[\\s]*\\?[\\s]*\\)[\\s]*");

        private static final Pattern WHERE_PATTERN_NO_PARAM_IN_BRACKETS =
                Pattern.compile("[\\s]*\\([\\s]*name[\\s]*=[\\s]*['\"].*['\"][\\s]*\\)[\\s]*");

        private static final Pattern WHERE_PATTERN_NO_PARAM_NO_BRACKETS =
                Pattern.compile("[\\s]*name[\\s]*=[\\s]*['\"].*['\"][\\s]*");

        public final String name;
        public final boolean isValid;

        public Arguments(Uri uri, String where, String[] whereArgs, boolean supportAll) {
            final int segmentSize = uri.getPathSegments().size();
            switch (segmentSize) {
                case 1: {
                    if (where != null
                            && (WHERE_PATTERN_WITH_PARAM_NO_BRACKETS.matcher(where).matches()
                                || WHERE_PATTERN_WITH_PARAM_IN_BRACKETS.matcher(where).matches())
                            && whereArgs.length == 1) {
                        name = whereArgs[0];
                        isValid = true;
                        return;
                    } else if (where != null
                            && (WHERE_PATTERN_NO_PARAM_NO_BRACKETS.matcher(where).matches()
                                || WHERE_PATTERN_NO_PARAM_IN_BRACKETS.matcher(where).matches())) {
                        final int startIndex = Math.max(where.indexOf("'"),
                                where.indexOf("\"")) + 1;
                        final int endIndex = Math.max(where.lastIndexOf("'"),
                                where.lastIndexOf("\""));
                        name = where.substring(startIndex, endIndex);
                        isValid = true;
                        return;
                    } else if (supportAll && where == null && whereArgs == null) {
                        name = null;
                        isValid = true;
                        return;
                    }
                } break;

                case 2: {
                    if (where == null && whereArgs == null) {
                        name = uri.getPathSegments().get(1);
                        isValid = true;
                        return;
                    }
                } break;
            }

            String message = String.format( "Supported SQL:\n"
                    + "  uri content://some_table/some_property with null where and where args\n"
                    + "  uri content://some_table with query name=? and single name as arg\n"
                    + "  uri content://some_table with query name=some_name and null args\n"
                    + "  but got - uri:%1s, where:%2s whereArgs:%3s", uri, where,
                    Arrays.toString(whereArgs));
            throw new IllegalArgumentException(message);
        }
    }

    final class ConfigsRegistry {
        private static final String CONFIGS_DIR = "configs";
        private static final String CONFIG_FILE = "config.xml";
        private ConfigsState configsState;
        private final Handler mHandler;

        public ConfigsState getConfigsStateLocked() {
            ensureConfigsStateLocked();
            return configsState;
        }

        public ConfigsRegistry() {
            mHandler = new ConfigsProvider.ConfigsRegistry.MyHandler(getContext().getMainLooper());
        }

        private void ensureConfigsStateLocked() {
            if (null == configsState) {
                configsState = new ConfigsState(mLock, getConfigsFile());
            }
        }

        public boolean insertConfigLocked(String name, String value) {
            ensureConfigsStateLocked();
            final boolean success = configsState.insertConfigLocked(name, value);
            if (success) {
                notifyForConfigsChange(name);
            }
            return success;
        }

        public boolean deleteConfigLocked(String name) {
            ensureConfigsStateLocked();
            final boolean success = configsState.deleteConfigLocked(name);
            if (success) {
                notifyForConfigsChange(name);
            }
            return success;
        }

        public ConfigsState.Config getConfigLocked(String name) {
            ensureConfigsStateLocked();
            return configsState.getConfigLocked(name);
        }

        public boolean updateConfigLocked(String name, String value) {
            ensureConfigsStateLocked();
            final boolean success = configsState.updateConfigLocked(name, value);
            if (success) {
                notifyForConfigsChange(name);
            }

            return success;
        }

        private void notifyForConfigsChange(String name) {
            Uri uri = getNotificationUriFor(name);
            mHandler.obtainMessage(ConfigsProvider.ConfigsRegistry.MyHandler.MSG_NOTIFY_URI_CHANGED, 0, 0, uri).sendToTarget();
        }

        private File getConfigsFile() {
            File file = new File(getContext().getFilesDir(), CONFIGS_DIR);
            return new File(file, CONFIG_FILE);
        }

        private Uri getNotificationUriFor(String name) {
            return Uri.withAppendedPath(Configs.CONTENT_URI, name);
        }

        private final class MyHandler extends Handler {
            private static final int MSG_NOTIFY_URI_CHANGED = 1;

            public MyHandler(Looper looper) {
                super(looper);
            }

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_NOTIFY_URI_CHANGED: {
                        Uri uri = (Uri) msg.obj;
                        getContext().getContentResolver().notifyChange(uri, null);
                        if (DEBUG) {
                            Log.v(LOG_TAG, "Notifying for " + uri);
                        }
                    } break;
                }
            }
        }
    }
}
